package pers.whj.net.local.client;

import pers.whj.net.local.exception.ErrorCodeEnum;
import pers.whj.net.local.exception.WHJException;
import pers.whj.net.local.protocol.Protocol;
import pers.whj.net.local.task.Task;
import pers.whj.net.local.tools.NetworkTools;
import pers.whj.net.local.task.FileInfo;
import pers.whj.net.local.task.FileInfoTask;
import pers.whj.net.local.task.FileTask;

import java.io.*;
import java.net.*;
import java.util.List;

public class Client {
    protected byte[] mData;
    protected DataInputStream mInput;
    protected DataOutputStream mOutput;
    private static String sDefaultServerIP;
    private static String sClientName;
    private final Socket mSocket;
    private InetSocketAddress mServerAddress;

    public Client(String name, String defaultServerIP) throws WHJException {
        if (name == null || name.length() == 0) {
            throw new WHJException(ErrorCodeEnum.EMPTY_CLIENT_NAME);
        }
        mSocket = new Socket();
        mServerAddress = null;
        sClientName = name;
        sDefaultServerIP = defaultServerIP;
    }

    public final boolean createConnection() throws WHJException {
        if (connectionCreated()) {
            return true;
        }
        if (init() && mServerAddress != null) {
            try {
                mSocket.connect(mServerAddress, Protocol.TIMEOUT_TCP_CONNECT);
                mSocket.setSoTimeout(Protocol.TIMEOUT_TCP_READ);
                mInput = new DataInputStream(mSocket.getInputStream());
                mOutput = new DataOutputStream(mSocket.getOutputStream());
                mData = new byte[Protocol.LENGTH_READ];
                initClientName();
                return true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    public final void endConnection() {
        try {
            mServerAddress = null;
            if (mOutput != null) {
                mOutput.writeByte(Protocol.END_CONNECTION);
            }
            mSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public final boolean connectionCreated() {
        if (mSocket.isClosed() || !mSocket.isConnected()) {
            return false;
        }
        try {
            mSocket.setSoTimeout(Protocol.TIMEOUT_CHECK_STATE);
            mOutput.writeByte(Protocol.CHECK_STATE);
            byte command = mInput.readByte();
            mSocket.setSoTimeout(Protocol.TIMEOUT_TCP_READ);
            return command == Protocol.CHECK_STATE;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public String getClientName() {
        return sClientName;
    }

    public String getDefaultServerIP() {
        return sDefaultServerIP;
    }

    public boolean executeTask(Task task) throws IOException, WHJException {
        if (task != null) {
            if (task instanceof FileTask) {
                return executeFileTask((FileTask) task);
            }
            if (task instanceof FileInfoTask) {
                return executeFileInfoTask((FileInfoTask) task);
            }
        }
        return false;
    }

    protected final byte[] readData(int length) throws IOException {
        byte[] data = new byte[length];
        int receive = 0;
        int maxLength = Protocol.LENGTH_READ;
        while (length > maxLength) {
            readData(data, receive, maxLength);
            receive += maxLength;
            length -= maxLength;
        }
        readData(data, receive, length);
        return data;
    }

    protected final void readData(byte[] data, int offset, int length) throws IOException {
        int receive = 0;
        while (length > 0) {
            int len = mInput.read(data, offset + receive, length);
            receive += len;
            length -= len;
        }
    }

    private boolean executeFileInfoTask(FileInfoTask task) throws WHJException, IOException {
        if (task.getType() == FileInfoTask.CREATE_REMOTE_FILE) {
            return createRemoteFile(task);
        }
        String remoteFilePath = task.getRemoteFilePath();
        String newRemoteFilePath = task.getNewRemoteFilePath();
        if (remoteFilePath == null) {
            throw new WHJException(ErrorCodeEnum.REMOTE_EMPTY_FILE_NAME);
        }
        //request for operation
        byte[] data = remoteFilePath.getBytes();
        switch (task.getType()) {
            case FileInfoTask.GET_REMOTE_FILE_INFO:
                mOutput.writeByte(Protocol.REQ_REMOTE_FILE_INFO);
                break;
            case FileInfoTask.DELETE_REMOTE_FILE:
                mOutput.writeByte(Protocol.REQ_DELETE_REMOTE_FILE);
                break;
            case FileInfoTask.RENAME_REMOTE_FILE:
                if (newRemoteFilePath == null || newRemoteFilePath.length() == 0) {
                    return false;
                }
                mOutput.writeByte(Protocol.REQ_RENAME_REMOTE_FILE);
                break;
            default:
                return false;
        }
        mOutput.writeInt(data.length);
        mOutput.write(data, 0, data.length);
        if (task.getType() == FileInfoTask.RENAME_REMOTE_FILE) {
            data = newRemoteFilePath.getBytes();
            mOutput.writeInt(data.length);
            mOutput.write(data, 0, data.length);
        }
        mOutput.flush();
        //check response
        byte command = mInput.readByte();
        switch (task.getType()) {
            case FileInfoTask.GET_REMOTE_FILE_INFO:
                if (command == Protocol.RESPONSE) {
                    while (mInput.readByte() == Protocol.DATA) {
                        data = readData(mInput.readInt());
                        remoteFilePath = new String(data, 0, data.length);
                        boolean isFile = mInput.readBoolean();
                        long fileLength = mInput.readLong();
                        FileInfo fileInfo = new FileInfo(remoteFilePath, isFile, fileLength);
                        if (remoteFilePath.equals(task.getRemoteFilePath())) {
                            task.setFileInfo(fileInfo);
                        } else {
                            task.getFileInfo().addFileInfo(fileInfo);
                        }
                    }
                    return true;
                }
                break;
            case FileInfoTask.DELETE_REMOTE_FILE:
            case FileInfoTask.RENAME_REMOTE_FILE:
                if (command == Protocol.SUCCESS) {
                    return true;
                }
                break;
            default:
                return false;
        }
        throw new WHJException(ErrorCodeEnum.getByCode(mInput.readInt()));
    }

    private boolean executeFileTask(FileTask task) throws IOException, WHJException {
        switch (task.getType()) {
            case FileTask.UPLOAD_FILE:
                return upload(task);
            case FileTask.DOWNLOAD_FILE:
                return download(task);
            default:
                return false;
        }
    }

    private boolean createRemoteFile(FileInfoTask task) throws IOException, WHJException {
        List<FileInfoTask.CreationInfo> list = task.getCreationInfoList();
        if (list == null) {
            return false;
        }
        if (list.size() == 0) {
            return true;
        }
        mOutput.writeByte(Protocol.REQ_CREATE_REMOTE_FILE);
        byte command = mInput.readByte();
        if (command == Protocol.RESPONSE) {
            for (FileInfoTask.CreationInfo creationInfo : list) {
                if (creationInfo == null) {
                    continue;
                }
                if (creationInfo.isValidFilePath()) {
                    byte[] data = creationInfo.getFilePath().getBytes();
                    mOutput.writeByte(Protocol.DATA);
                    mOutput.writeInt(data.length);
                    mOutput.write(data, 0, data.length);
                    mOutput.writeBoolean(creationInfo.isDirectory());
                    mOutput.flush();
                    //check if successfully created
                    creationInfo.setSuccess(mInput.readByte() == Protocol.SUCCESS);
                } else {
                    creationInfo.setSuccess(false);
                }
            }
            mOutput.writeByte(Protocol.DATA_END);
            return true;
        }
        throw new WHJException(ErrorCodeEnum.getByCode(mInput.readInt()));
    }

    private boolean upload(FileTask task) throws IOException, WHJException {
        File file = task.getFile();
        String remoteFilePath = task.getRemoteFilePath();
        if (file == null || !file.exists() || !file.canRead()) {
            return false;
        }
        if (file.isDirectory()) {
            throw new WHJException(ErrorCodeEnum.FILE_DIRECTORY);
        }
        if (!connectionCreated()) {
            throw new WHJException(ErrorCodeEnum.DISCONNECTION);
        }
        if (remoteFilePath == null) {
            throw new WHJException(ErrorCodeEnum.REMOTE_EMPTY_FILE_NAME);
        }
        //request for upload
        long fileSize = file.length();
        byte[] data = remoteFilePath.getBytes();
        task.setLength(fileSize);
        mOutput.writeByte(Protocol.REQ_UPLOAD);
        mOutput.writeInt(data.length);
        mOutput.write(data, 0, data.length);
        mOutput.writeLong(fileSize);
        mOutput.flush();
        //check response
        byte command = mInput.readByte();
        if (command == Protocol.RESPONSE) {
            FileInputStream fileInput = new FileInputStream(file);
            int len;
            while ((len = fileInput.read(mData, 0, mData.length)) != -1) {
                mOutput.write(mData, 0, len);
                mOutput.flush();
                task.addCurrentLength(len);
            }
            try {
                fileInput.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            //check if success
            command = mInput.readByte();
            if (command == Protocol.SUCCESS) {
                return true;
            } else if (command == Protocol.FAIL) {
                throw new WHJException(ErrorCodeEnum.getByCode(mInput.readInt()));
            }
        } else if (command == Protocol.FAIL) {
            throw new WHJException(ErrorCodeEnum.getByCode(mInput.readInt()));
        }
        return false;
    }

    private boolean download(FileTask task) throws IOException, WHJException {
        File file = task.getFile();
        String remoteFilePath = task.getRemoteFilePath();
        if (file == null) {
            return false;
        }
        if (file.isDirectory()) {
            throw new WHJException(ErrorCodeEnum.FILE_DIRECTORY);
        }
        int parentLevel = 0;
        File parentFile = file.getParentFile();
        if (parentFile != null && !parentFile.exists()) {
            parentLevel++;
            parentFile = parentFile.getParentFile();
        }
        if (parentLevel > 0) {
            if (!file.getParentFile().mkdirs()) {
                return false;
            }
        }
        if (!file.exists() && !file.createNewFile()) {
            return false;
        }
        if (remoteFilePath == null) {
            throw new WHJException(ErrorCodeEnum.REMOTE_EMPTY_FILE_NAME);
        }
        FileOutputStream fileOutput = new FileOutputStream(file);
        //request for download
        byte[] data = remoteFilePath.getBytes();
        mOutput.writeByte(Protocol.REQ_DOWNLOAD);
        mOutput.writeInt(data.length);
        mOutput.write(data, 0, data.length);
        mOutput.flush();
        //check response
        byte command = mInput.readByte();
        if (command == Protocol.RESPONSE) {
            data = readData(mInput.readInt());
            String info = new String(data, 0, data.length);
            long fileSize;
            try {
                fileSize = Long.parseLong(info);
                task.setLength(fileSize);
            } catch (Exception e) {
                e.printStackTrace();
                endDownloadFile(file, fileOutput, false, parentLevel);
                return false;
            }
            int maxLen = Protocol.LENGTH_READ;
            while (fileSize > 0) {
                int realLen = (int) Math.min(fileSize, maxLen);
                try {
                    readData(mData, 0, realLen);
                } catch (IOException e) {
                    endDownloadFile(file, fileOutput, false, parentLevel);
                    throw e;
                }
                try {
                    fileOutput.write(mData, 0, realLen);
                } catch (IOException e) {
                    endDownloadFile(file, fileOutput, false, parentLevel);
                    throw e;
                }
                task.addCurrentLength(realLen);
                fileSize -= realLen;
            }
            command = mInput.readByte();
            if (command == Protocol.SUCCESS) {
                endDownloadFile(file, fileOutput, true, parentLevel);
                return true;
            }
        }
        endDownloadFile(file, fileOutput, false, parentLevel);
        throw new WHJException(ErrorCodeEnum.getByCode(mInput.readInt()));
    }

    private void endDownloadFile(File file, FileOutputStream fileOutput, boolean downloadSuccess, int parentLevel) {
        try {
            if (fileOutput != null) {
                fileOutput.close();
            }
            if (!downloadSuccess) {
                boolean flag = file != null && file.exists() && file.delete();
                if (flag) {
                    deleteEmptyDirectory(file.getParentFile(), parentLevel);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void deleteEmptyDirectory(File file, int parentLevel) {
        if (parentLevel > 0 && file != null && file.exists() && file.isDirectory()) {
            parentLevel--;
            String[] list = file.list();
            if (list != null && list.length == 0 && file.delete()) {
                deleteEmptyDirectory(file.getParentFile(), parentLevel);
            }
        }
    }

    private void initClientName() throws IOException, WHJException {
        byte[] data = sClientName.getBytes();
        mOutput.writeInt(data.length);
        mOutput.write(data, 0, data.length);
        mOutput.flush();
        byte command = mInput.readByte();
        if (command != Protocol.SUCCESS) {
            throw new WHJException(ErrorCodeEnum.FAIL_INIT_CLIENT_NAME);
        }
    }

    private boolean init() throws WHJException {
        DatagramSocket ds;
        boolean success = false;
        try {
            ds = new DatagramSocket(0);
        } catch (SocketException e) {
            throw new WHJException(e);
        }
        if (sDefaultServerIP != null) {
            success = initUDPConnection(ds, sDefaultServerIP);
        }
        if (!success) {
            List<String> broadcastAddresses;
            try {
                broadcastAddresses = NetworkTools.getBroadcastAddresses();
            } catch (SocketException e) {
                throw new WHJException(e);
            }
            for (String ip : broadcastAddresses) {
                success = initUDPConnection(ds, ip);
                if (success) {
                    break;
                }
            }
        }
        if (success) {
            sDefaultServerIP = mServerAddress.getAddress().getHostAddress();
            return true;
        }
        throw new WHJException(ErrorCodeEnum.NOT_FIND_SERVER);
    }

    private boolean initUDPConnection(DatagramSocket ds, String ip) {
        int counts = Protocol.RECONNECT_UDP;
        while (counts > 0) {
            try {
                byte[] data = Protocol.getData(Protocol.REQ_PORT, "");
                DatagramPacket dp = new DatagramPacket(data, 0, data.length
                        , InetAddress.getByName(ip), Protocol.PORT_UDP_SERVER);
                ds.send(dp);
                data = new byte[Protocol.LENGTH_READ];
                dp = new DatagramPacket(data, 0, data.length);
                ds.setSoTimeout(Protocol.TIMEOUT_UDP_CLIENT);
                int packetCounts = Protocol.MAX_PACKETS_UDP;
                while (packetCounts > 0) {
                    try {
                        ds.receive(dp);
                    } catch (SocketTimeoutException e) {
                        e.printStackTrace();
                        break;
                    }
                    data = dp.getData();
                    if (Protocol.getCommand(data) == Protocol.RES_PORT) {
                        InetAddress inetAddress = dp.getAddress();
                        int port = Integer.parseInt(Protocol.getInfo(data, dp.getLength()));
                        mServerAddress = new InetSocketAddress(inetAddress, port);
                        return true;
                    }
                    packetCounts--;
                }
            } catch (IOException | NumberFormatException e) {
                e.printStackTrace();
            }
            counts--;
        }
        return false;
    }
}